高级表格:列拖拽实现(SortableJS)
概述
借助 SortableJS 库实现高级表格的列拖拽功能,用户可以通过拖拽表头调整列的显示顺序。本节讲解 SortableJS 的核心 API、列拖拽的实现原理以及索引计算的注意事项。
SortableJS 简介
SortableJS 是一个轻量级的拖拽排序库,兼容 IE9+ 浏览器。
核心特性
| 特性 | 说明 |
|---|---|
| 零依赖 | 不依赖任何框架 |
| 动画支持 | 拖拽过程中自带平滑动画 |
| 样式保持 | 拖拽时保持原有列表样式 |
| 事件回调 | onEnd 回调提供 oldIndex 和 newIndex |
安装
pnpm add sortablejs
bash
基础用法
import Sortable from 'sortablejs'
const el = document.getElementById('list')
Sortable.create(el, {
animation: 150,
onEnd(evt) {
const { oldIndex, newIndex } = evt
// 处理排序逻辑
},
})
typescript
列拖拽实现
绑定拖拽到表头
import Sortable from 'sortablejs'
import type { SortableEvent } from 'sortablejs'
function initColumnDrag(tableRef: Ref<any>, columns: Ref<TableColumn[]>) {
nextTick(() => {
const tableEl = tableRef.value?.$el
if (!tableEl) return
const headerTr = tableEl.querySelector(
'.el-table__header-wrapper thead tr'
)
if (!headerTr) return
Sortable.create(headerTr, {
animation: 150,
delay: 0,
onEnd(event: SortableEvent) {
const { oldIndex, newIndex } = event
if (oldIndex === newIndex) return
// 重排 columns
const movedColumn = columns.value.splice(oldIndex!, 1)[0]
columns.value.splice(newIndex!, 0, movedColumn)
},
})
})
}
typescript
模板集成
<template>
<el-table ref="tableRef" :data="tableData">
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
/>
</el-table>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import type { TableColumn } from '@/types'
const tableRef = ref()
const columns = ref<TableColumn[]>([
{ prop: 'id', label: 'ID', width: 80 },
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'date', label: '日期', width: 150 },
{ prop: 'address', label: '地址' },
])
const tableData = ref([])
onMounted(() => {
initColumnDrag(tableRef, columns)
})
</script>
vue
索引计算注意事项
oldIndex 与 newIndex 的含义
拖拽前: [A, B, C, D, E]
↑
oldIndex=1 (B)
拖拽后: [A, C, D, B, E]
↑
newIndex=3 (B 插入位置)
text
边界情况处理
onEnd(event: SortableEvent) {
const { oldIndex, newIndex } = event
// 1. 位置未变化,跳过
if (oldIndex === newIndex || oldIndex == null || newIndex == null) return
// 2. 从小索引移到大索引
// oldIndex < newIndex: 移除元素后,后续元素前移
// 需要先 splice 移除,再 splice 插入
// 3. 从大索引移到小索引
// oldIndex > newIndex: 插入元素后,后续元素后移
// 处理方式相同
const movedItem = columns.value.splice(oldIndex, 1)[0]
columns.value.splice(newIndex, 0, movedItem)
}
typescript
与固定列的兼容
当表格存在固定列(fixed)时,SortableJS 操作的是 DOM 节点,但 El-Table 的固定列会复制一份 DOM:
// 需要注意固定列的影响
// 如果有 fixed 列,el-table 会生成多组 thead
// 需要选择正确的 thead tr
const headerTr = tableEl.querySelector(
'.el-table__header-wrapper:not(.is-fixed) thead tr'
)
typescript
SortableJS 常用配置
Sortable.create(el, {
animation: 150, // 动画时长(ms)
delay: 0, // 拖拽延迟
handle: '.drag-handle', // 拖拽手柄选择器
filter: '.no-drag', // 禁止拖拽的元素
draggable: 'th', // 可拖拽的子元素选择器
ghostClass: 'sortable-ghost', // 拖拽占位样式
chosenClass: 'sortable-chosen', // 选中样式
dragClass: 'sortable-drag', // 拖拽中样式
onEnd(evt) { // 拖拽结束回调
// evt.oldIndex, evt.newIndex
},
})
typescript
实践要点
- 列拖拽需要绑定到
el-table的thead tr元素上,而非tbody nextTick确保表格 DOM 渲染完成后再初始化 SortableJS- 列重排后需要更新响应式的
columns数组,触发 Vue 重新渲染 - 固定列场景需特殊处理,避免操作到固定列的复制 DOM
- 拖拽结束后建议 emit 事件通知父组件,支持持久化用户的列顺序偏好
↑